/ctfs/picoctf - 2021/web/super serial

Synopsis



Recon

Index page is a login page, but there is no endpoint to register...


I tried quickly to bypass it but nothing. Hopefully I found robots.txt


Damn... the page doesn't exist...


Get the source code

The synopsis talks about serialization, this is barely impossible without the source code so there must be a way to get it...
Okay so I thought again about the "admin.phps", maybe the "s" was there for "source" so I tried to get "index.phps"


Yes ! Thanks to that technique, I have been able to get the source code of 3 pages.

index.php

<?php
            require_once("cookie.php");

            if(isset($_POST["user"]) && isset($_POST["pass"])){
                $con = new SQLite3("users.db");
                $username = $_POST["user"];
                $password = $_POST["pass"];
                $perm_res = new permissions($username, $password);
                echo serialize($perm_res);
                if ($perm_res->is_guest() || $perm_res->is_admin()) {
                    setcookie("login", urlencode(base64_encode(serialize($perm_res))), time() + (86400 * 30), "/");
                    header("Location: authentication.php");
                    die();
                } else {
                    $msg = '<h6 class="text-center" style="color:red">Invalid Login.</h6>';
                }
            }
            ?>

            ...
            

cookie.php

<?php
            session_start();

            class permissions
            {
                public $username;
                public $password;

                function __construct($u, $p) {
                    $this->username = $u;
                    $this->password = $p;
                }

                function __toString() {
                    return $u.$p;
                }

                function is_guest() {
                    $guest = false;

                    $con = new SQLite3("users.db");
                    $username = $this->username;
                    $password = $this->password;
                    $stm = $con->prepare("SELECT admin, username FROM users WHERE username=? AND password=?");
                    $stm->bindValue(1, $username, SQLITE3_TEXT);
                    $stm->bindValue(2, $password, SQLITE3_TEXT);
                    $res = $stm->execute();
                    $rest = $res->fetchArray();
                    if($rest["username"]) {
                        if ($rest["admin"] != 1) {
                            $guest = true;
                        }
                    }
                    return $guest;
                }

                    function is_admin() {
                            $admin = false;

                            $con = new SQLite3("users.db");
                            $username = $this->username;
                            $password = $this->password;

                            $stm = $con->prepare("SELECT admin, username FROM users WHERE username=? AND password=?");
                            $stm->bindValue(1, $username, SQLITE3_TEXT);
                            $stm->bindValue(2, $password, SQLITE3_TEXT);
                            $res = $stm->execute();
                            $rest = $res->fetchArray();
                            echo $res;
                            if($rest["username"]) {
                                    if ($rest["admin"] == 1) {
                                            $admin = true;
                                    }
                            }
                            return $admin;
                    }
            }

            if(isset($_COOKIE["login"])){
                try{
                    $perm = unserialize(base64_decode(urldecode($_COOKIE["login"])));
                    $g = $perm->is_guest();
                    $a = $perm->is_admin();
                }
                catch(Error $e){
                    die("Deserialization error. ".$perm);
                }
            }

            ?>
            

authentification.php

<?php

            class access_log
            {
                public $log_file;

                function __construct($lf) {
                    $this->log_file = $lf;
                }

                function __toString() {
                    return $this->read_log();
                }

                function append_to_log($data) {
                    file_put_contents($this->log_file, $data, FILE_APPEND);
                }

                function read_log() {
                    return file_get_contents($this->log_file);
                }
            }

            require_once("cookie.php");
            if(isset($perm) && $perm->is_admin()){
                $msg = "Welcome admin";
                $log = new access_log("access.log");
                $log->append_to_log("Logged in at ".date("Y-m-d")."\n");
            } else {
                $msg = "Welcome guest";
            }
            ?>

            ...
            


Exploit php deserialization

deserialization is a well known php vuln. If the user is able to manipulate the string which is going to be deserialize, it could conducts to not exepected behaviors or even RCE.

Here, we need to focus on this part of the code

            ...
            if(isset($_COOKIE["login"])){
                try{
                    $perm = unserialize(base64_decode(urldecode($_COOKIE["login"])));
                    $g = $perm->is_guest();
                    $a = $perm->is_admin();
                }
                catch(Error $e){
                    die("Deserialization error. ".$perm);
                }
            }
            

You can see that if an error occured, a message error is printed but the code concatenate the unserialize object to this message. This is where the problem is... This is going to call the __toString() function of the object
Normally, this is done to print the "permission" object but...


            class access_log
            {
                public $log_file;

                function __construct($lf) {
                    $this->log_file = $lf;
                }

                function __toString() {
                    return $this->read_log();
                }

            ...
            

Yes, "access_log" class also got a __toString() function which even permits to read a file !
If you a have a cookie like that, you have normal access to the page.


But if You use a cookie containing an "access_log" serialize object, the deserialization is gonna failed (because "access_log" doesn't have "is_guest" function) and, its __toString() result will be concatenate to the error message.
So if we pass the flag's file as "log_file" we'll be able to read it. See what happens next...

And we are done, picoCTF{th15_vu1n_1s_5up3r_53r1ous_y4ll_66832978}